#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <functional>
#include <unordered_map>

#include "Log.hpp"

// using func_t = std::function<std::string(const std::string&)>; //c++11包装器
// typedef std::function<std::string(const std::string &)> func_t;
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0"; // 设置为0，表示任意地址绑定
const int size = 1024;

class Udpserver
{
public:
    Udpserver(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : _sockfd(0), _port(port), _ip(ip), _isrunning(false)
    {
    }

    void Init()
    {
        // 1，创建Udp套接字，Udp的socket是全双工的，允许被同时读写的
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 表示使用IPv4协议，类型为Udp用户数据报协议
        if (_sockfd < 0)                          // 创建失败
        {
            Log("socket create error", Error) << "\n";
            exit(SOCKET_ERR);
        }
        Log("socket create success", Debug) << "\n"; // 创建成功，输出日志
        // 2，创建和填充sockaddr结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 把指定类型的指定大小初始化为0，功能类似于memset
        // 然后就是将我们自己的服务器的一些信息填充进这个结构体里，方便socket API使用
        local.sin_family = AF_INET; // 表明自身的结构体类型为IPv4   这个family是在宏定义用##来实现的

        // local.sin_port = _port; //表明服务器将来要绑定的端口号 -- 需要保证我的端口号是网络字节序列，因为该端口号是要给对方发送的
        // 除了发正常消息外，我也要把我的端口号发过去，这样对面发给我的时候才能找到我，所以端口号需要发送到网络里的，所以一开始我们把这个东东填充到结构体里时，必须是网络字节序
        local.sin_port = htons(_port); // 把主机序列转化成网络序列，大端不变，小端会转大端

        // local.sin_addr = _ip;  //s表示socket，然后in表示inet，addr表示IP地址（ifconfig命令）
        // 我们需要先把string的ip --> uint32_t的ip，并且必须是网络序列的，而这样的各种转化都是有相应的接口的，不需要我们自己写了
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr表示把字符串转为网络字节序列也就是uint32_t
        // 查看sockaddr_in的定义后可以发现，sin_addr其实是一个struct类型，这个类型里的s_addr才是要转化的类型
        //  local.sin_addr.s_addr = htonl(INADDR_ANY); // 这个宏表示任意ip地址，数值为0

        // 3，绑定套接字
        //  local是在地址空间的用户栈上定义的，上面三个参数的所有操作都是在栈上填好，并没有将local与网络套接字socket相关联，所以需要绑定bind函数
        int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            Log("port bind error", Error) << "\n";
            exit(BIND_ERR);
        }
        Log("port bind success", Debug) << "\n";
        std::cout << "Waiting user to connect ... " << std::endl;
    }

    void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport) // 检查是否为新用户
    {

        auto iter = _online_user.find(clientip); // 去哈希表去找对应IP的信息
        if (iter == _online_user.end())          // 如果上面这个查找的迭代器走到了结尾，说明哈希表里还没有这个ip，添加
        {
            _online_user.insert({clientip, client}); // 把ip和对应的套接字结构体插入
            std::cout << "[" << clientip << ": " << clientport << "]add to online user" << std::endl;
        }
        else // 如果存在了，则什么都不做
        {
        }
    }

    void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
    {
        for (const auto &user : _online_user) // 遍历在线用户，遍历发送
        {
            // 编辑发送形式
            std::string message = "[";
            message += clientip;
            message += ":";
            message += std::to_string(clientport);
            message += "]#";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)(&user.second), len); // sendto发送回给对方
        }
    }

    // void Run(func_t func) // 对代码进行分层,场景一用这条语句，模拟群聊用下面那个
    // 模拟群聊，上面的是run函数执行特定的处理方法，可以根据需要自由选择
    void Run()
    {
        _isrunning = true;
        char inbuffer[size]; //
        while (_isrunning)   // 服务器应该能周而复始一直在运行的，所以是死循环
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 读取数据，从指定套接字里读取消息，然后把读取到的数据放到缓冲区中并指名长度，最后两个参数作为输出型参数，保存对方的IP和port等信息，方便后面我发消息回去
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                Log("recvfrom error", Warning) << "\n";
                continue;
            }

            // 模拟群聊
            // ①拿到各客户端的ip和端口号
            uint16_t clientport = ntohs(client.sin_port);      // 拿到客户端的端口号，网络序列转主机序列
            std::string clientip = inet_ntoa(client.sin_addr); // 那搭配客户端ip地址，把inet_ntoa四字节ip转化为char*
            // ②把新用户的ip信息添加到unordered_map里去
            CheckUser(client, clientip, clientport);
            // ③把消息转发给所有人，让当前在线的用户都能看到
            std::string info = inbuffer;
            Broadcast(info, clientip, clientport); // 是clientip发的消息，端口号为clientport，发的消息是info
        }
    }
    ~Udpserver()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }

private:
    int _sockfd;                                                      // 网络文件描述符
    uint16_t _port;                                                   // 表明服务器进程的端口号
    std::string _ip;                                                  // 地址绑定
    bool _isrunning;                                                  // 表明服务器是否在运行状态
    std::unordered_map<std::string, struct sockaddr_in> _online_user; // 第一个键值是ip，第二个键值是ip对应的套接字结构体信息
};
